<template>
    <view class="componentBox">
        <view class="microphoneBox line-item-center">
            <view class="line-item-center flex-column" v-if="!running" @click="bindStart">
                <view>
                    <view class="text">开始录音</view>
                </view>
            </view>
            <view class="line-item-center flex-column" v-else>
                <view class="resultBox" v-if="result">
                    <scroll-view class="scroll-view" scroll-y>
                        <view id="resultPage" class="resultPage">{{ result }}</view>
                    </scroll-view>
                </view>
                <view class="line-item-center flex-column" @click="bindEnd">
                    <view>停止录音</view>
                    <view class="text">{{ Number(time / 1000).toFixed(1) }}s</view>
                </view>
            </view>
        </view>
    </view>
</template>

<script>
/**
 * @author: 1580043700@qq.com - Zhang Jun
 * @date: 2022/04/13 10:05
 * @description: 实时语音转写组件 - 科大讯飞版
 */

let CryptoJS = requirePlugin("CryptoJs");

let recorderManager = uni.getRecorderManager();
let wxst = null;
let iatResult = [];
let interval = null;

// 帧定义
const FRAME = {
    STATUS_FIRST_FRAME: 0,
    STATUS_CONTINUE_FRAME: 1,
    STATUS_LAST_FRAME: 2,
};
let status = FRAME.STATUS_FIRST_FRAME;

export default {
    props: {},
    data() {
        return {
            running: false,
            time: 0,
            result: "",
            url: "",
            config: {
                // 请求地址
                hostUrl: "wss://iat-api.xfyun.cn/v2/iat",
                host: "iat-api.xfyun.cn",
                uri: "/v2/iat",
                //TODO 在控制台-我的应用-语音听写（流式版）获取
                appid: "",
                apiSecret: "",
                apiKey: "",
            },
        };
    },
    created() {},
    mounted() {
        recorderManager.onFrameRecorded(res => {
            this.send(res.frameBuffer);
        });

        recorderManager.onStart(res => {
            this.running = true;
            status = FRAME.STATUS_FIRST_FRAME;
            this.time = 0;
            this.title = "";
            this.result = "";
            iatResult = [];
            console.warn("recorder onStart", res);
            this.bindInterval();
        });

        recorderManager.onStop(res => {
            uni.hideLoading();
            this.running = false;
            this.bindClearInterval();
            console.error("recorder onStop", res);

            status = FRAME.STATUS_LAST_FRAME;
            this.send("");
            uni.showLoading({
                title: "转换中...",
                mask: true,
            });
            this.PCM2Wav(
                res.tempFilePath,
                filePath => {
                    uni.hideLoading();
                    this.url = filePath;

                    const fileSystemManager = uni.getFileSystemManager();
                    fileSystemManager.removeSavedFile({
                        filePath: res.tempFilePath,
                    });
                    // fileSystemManager.removeSavedFile({
                    //     filePath: filePath,
                    // });
                },
                () => {
                    uni.showToast({
                        title: "录音文件转码失败",
                        icon: "none",
                    });
                }
            );
        });

        wx.onSocketOpen(res => {
            console.warn("onSocketOpen", res);
            recorderManager.start({
                duration: 60000, //指定录音的时长，单位 ms
                sampleRate: 16000, //采样率
                numberOfChannels: 1,
                format: "PCM", //音频格式
                frameSize: 100, //指定帧大小，单位 KB
            });
        });

        uni.onSocketMessage(res => {
            let result = JSON.parse(res.data);
            console.warn("onSocketMessage", result, res);
            if (result.data && result.code == 0) {
                let str = "";
                if (result.data.status == 2) {
                    // "最终识别结果"

                    iatResult[result.data.result.sn] = result.data.result;
                    iatResult.forEach(i => {
                        if (i != null) {
                            i.ws.forEach(j => {
                                j.cw.forEach(k => {
                                    str += k.w;
                                });
                            });
                        }
                    });
                    this.result = str;
                    this.$emit("stop", { text: str, url: this.url });
                    uni.closeSocket();
                    return;
                } else {
                    // "中间识别结果"

                    iatResult[result.data.result.sn] = result.data.result;
                    iatResult.forEach(i => {
                        if (i != null) {
                            i.ws.forEach(j => {
                                j.cw.forEach(k => {
                                    str += k.w;
                                });
                            });
                        }
                    });
                    this.result = str;
                    this.$emit("change", text);
                }
            }

            uni.hideLoading();

            if (result.code == "10105") {
                uni.showToast({
                    title: "语音识别,没有权限",
                    icon: "none",
                });
            } else if (result.code == "10106") {
                uni.showToast({
                    title: "语音识别,无效参数",
                    icon: "none",
                });
            } else if (result.code == "10107") {
                uni.showToast({
                    title: "语音识别,非法参数值",
                    icon: "none",
                });
            } else if (result.code == "10110") {
                uni.showToast({
                    title: "语音识别,无授权许可",
                    icon: "none",
                });
            } else if (result.code == "10700") {
                uni.showToast({
                    title: "语音识别,引擎错误",
                    icon: "none",
                });
            } else if (result.code == "10202") {
                uni.showToast({
                    title: "语音识别,连接错误",
                    icon: "none",
                });
            } else if (result.code == "10204") {
                uni.showToast({
                    title: "语音识别,写错误",
                    icon: "none",
                });
            } else if (result.code == "10205") {
                uni.showToast({
                    title: "语音识别,读错误",
                    icon: "none",
                });
            } else if (result.code == "16003") {
                uni.showToast({
                    title: "语音识别,基础组件异常",
                    icon: "none",
                });
            } else if (result.code == "10800") {
                uni.showToast({
                    title: "语音识别,超过授权的连接数",
                    icon: "none",
                });
            }
            if (this.running) {
                this.running = false;
                this.bindClearInterval();
            }
        });
        uni.onSocketClose(res => {
            uni.hideLoading();
            console.error("onSocketClose", res);
            uni.closeSocket();
            recorderManager?.stop();
            this.running = false;
            this.bindClearInterval();
        });
        uni.onSocketError(res => {
            console.error("onSocketError", res);
        });
    },
    methods: {
        bindStart() {
            let url = this.getURL();

            wxst = uni.connectSocket({
                url: url,
                method: "GET",
                success: () => {
                    console.warn("connectSocket");
                },
                fail: error => {
                    uni.showToast({
                        title: "创建Socket失败",
                        icon: "none",
                    });
                    console.error("connectSocket", error);
                },
            });
        },
        bindEnd() {
            if (this.time < 3000) {
                uni.showToast({
                    title: "说话时间太短了",
                    icon: "none",
                });
            } else {
                uni.showLoading({
                    title: "识别中...",
                });
                recorderManager.stop();
            }
        },
        uploadRecorder(url, okCall) {
            okCall(data);
            // uploadFile(url)
            // 	.then(data => {
            // 		okCall(data);
            // 	})
            // 	.catch(() => {
            // 		uni.hideLoading();
            // 		uni.showModal({
            // 			title: "提示",
            // 			content: "保存语音失败",
            // 			showCancel: false,
            // 			success: () => {
            // 				this.uploadRecorder(url, okCall);
            // 			},
            // 		});
            // 	});
        },
        bindInterval() {
            interval = setInterval(() => {
                this.time = Number(this.time) + 100;
            }, 100);
        },
        bindClearInterval() {
            interval && clearInterval(interval);
            interval = null;
        },
        // 一些工具函数，将数据进行转码、封装
        encodeWAV(samples, numChannels, sampleRate) {
            var buffer = new ArrayBuffer(44 + samples.byteLength);
            var view = new DataView(buffer);
            /* RIFF identifier */
            this.writeString(view, 0, "RIFF");
            /* RIFF chunk length */
            view.setUint32(4, 36 + samples.byteLength, true);
            /* RIFF type */
            this.writeString(view, 8, "WAVE");
            /* format chunk identifier */
            this.writeString(view, 12, "fmt ");
            /* format chunk length */
            view.setUint32(16, 16, true);
            /* sample format (raw) */
            view.setUint16(20, 1, true);
            /* channel count */
            view.setUint16(22, numChannels, true);
            /* sample rate */
            view.setUint32(24, sampleRate, true);
            /* byte rate (sample rate * block align) */
            view.setUint32(28, sampleRate * 4, true);
            /* block align (channel count * bytes per sample) */
            view.setUint16(32, numChannels * 2, true);
            /* bits per sample */
            view.setUint16(34, 16, true);
            /* data chunk identifier */
            this.writeString(view, 36, "data");
            /* data chunk length */
            view.setUint32(40, samples.byteLength, true);

            this.copyBytes(view, 44, samples);

            return view;
        },
        copyBytes(output, offset, input) {
            const dataView = new DataView(input);
            for (var i = 0; i < input.byteLength; i++, offset++) {
                output.setInt8(offset, dataView.getInt8(i));
            }
        },
        writeString(view, offset, string) {
            for (var i = 0; i < string.length; i++) {
                view.setUint8(offset + i, string.charCodeAt(i));
            }
        },
        PCM2Wav(filepath, okCal, errorCal) {
            const fileSystemManager = uni.getFileSystemManager();
            fileSystemManager.readFile({
                filePath: filepath,
                success: res => {
                    let view = this.encodeWAV(res.data, 1, 16000);
                    let dateNow = Date.now();
                    fileSystemManager.writeFile({
                        data: view.buffer,
                        filePath: `${wx.env.USER_DATA_PATH}/${dateNow}.wav`,
                        success: () => {
                            okCal(`${wx.env.USER_DATA_PATH}/${dateNow}.wav`);
                        },
                        fail: () => {
                            errorCal();
                        },
                    });
                },
            });
        },
        send(data) {
            let frame = "";
            let frameDataSection = {
                status: status,
                format: "audio/L16;rate=16000",
                audio: uni.arrayBufferToBase64(data),
                encoding: "raw",
            };
            switch (status) {
                case FRAME.STATUS_FIRST_FRAME:
                    frame = {
                        // 填充common
                        common: {
                            app_id: this.config.appid,
                        },
                        //填充business
                        business: {
                            language: "zh_cn",
                            domain: "iat",
                            accent: "mandarin",
                            dwa: "wpgs", // 可选参数，动态修正
                        },
                        //填充data
                        data: frameDataSection,
                    };
                    status = FRAME.STATUS_CONTINUE_FRAME;
                    break;
                case FRAME.STATUS_CONTINUE_FRAME:
                case FRAME.STATUS_LAST_FRAME:
                    //填充frame
                    frame = {
                        data: frameDataSection,
                    };
                    break;
            }
            console.warn("frame", frame);
            wxst.send({ data: JSON.stringify(frame) });
        },
        getURL() {
            const config = this.config;

            let date = new Date().toUTCString();
            return config.hostUrl + "?authorization=" + this.getAuthStr(date) + "&date=" + encodeURIComponent(date) + "&host=" + config.host;
        },
        // 鉴权签名
        getAuthStr(date) {
            const config = this.config;
            let signatureOrigin = `host: ${config.host}\ndate: ${date}\nGET ${config.uri} HTTP/1.1`;
            let signatureSha = CryptoJS.HmacSHA256(signatureOrigin, config.apiSecret);
            let signature = CryptoJS.enc.Base64.stringify(signatureSha);
            let authorizationOrigin = `api_key="${config.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
            let authStr = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(authorizationOrigin));
            return authStr;
        },
    },
    destroyed() {
        uni.hideLoading();
        uni.closeSocket();
        recorderManager?.stop();
        this.bindClearInterval();
    },
};
</script>

<style scoped lang="scss">
.componentBox {
    .microphoneBox {
        .resultBox {
            background-color: rgba(0, 0, 0, 0.3);
            border-radius: 20rpx;

            .scroll-view {
                font-size: 32rpx;
                padding: 20rpx;
                max-height: 300rpx;
                color: white;
            }
        }

        .text {
            font-size: 32rpx;
            text-align: center;
            padding: 10rpx 20rpx;
            min-width: 60rpx;
            border-radius: 40rpx;
            background-color: rgba(0, 0, 0, 0.2);
        }
    }
}
</style>
